package com.nado.extensions.util;

import org.apache.commons.lang.StringUtils;

import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;

public class Element<T> extends GenericMap<NodeAccess<T>> implements NodeAccess<T> {

	private static final Logger logger = Logger.getLogger("navis.internal.xml.mapper");

	public static final char DEFAULT_DELIMITER = '/';

	private String name;
	private char delimiter;

	private List<String> order = new ArrayList<String>();

	public Element(String name, char delimiter) {
		this.name = name;
		this.delimiter = delimiter;
	}

	public String name() {
		return name;
	}

	public void value(T value) {
		addElement("", new Attribute("", value));
	}

	public void attribute(String name, T value) {
		addElement("@" + name, new Attribute(name, value));
	}

	public Element<T> element(String name) {
		Element<T> child = new Element<T>(name, delimiter);
		addElement(name, child);
		return child;
	}

	public Element<T> addChild(String name, Element<T> child) {
		addElement(name, child);
		return this;
	}

	public ObjectNode<T> select(String expression) {
		return select(this, expression);
	}

	public List<ObjectNode<T>> selectAll(String expression) {
		return selectAll(Arrays.asList((NodeAccess<T>) this), expression);
	}

	@Override
	protected void addElement(String key, NodeAccess<T> value) {
		super.addElement(key, value);
		if (!key.startsWith("@")) {
			order.add(key);
		}
	}

	private List<Order> orderList() {
		List<Order> duplicated = new ArrayList<Order>(order.size());
		for (String key : order) {
			Order order = new Order(key);
			int prevIndex = duplicated.indexOf(order);
			if (prevIndex < 0) {
				duplicated.add(order);
			} else {
				duplicated.add(duplicated.get(prevIndex));
			}
		}
		return duplicated;
	}

	private Order order(int index) {
		if (index >= order.size()) {
			return new Order("!@#$%");  // invalid index
		}
		Order location = new Order(order.get(index));
		for (int i = 0; i < index; i++) {
			if (order.get(i).equals(location.key)) {
				location.increment();
			}
		}
		return location;
	}

	private ObjectNode<T> select(NodeAccess<T> start, String expression) {
		if (logger.isLoggable(Level.FINE)) {
		    logger.fine(" [Element/select] start " + start);
		}
		if (start == null || StringUtils.isEmpty(expression)) {
			return start;
		}
		List<NodeAccess<T>> currents = Arrays.asList(start);
		for (ObjectName childName : new ObjectName(expression, delimiter)) {
			if (logger.isLoggable(Level.FINE)) {
			    logger.fine(" -- [Element/select] iteration " + childName.name());
			}
			boolean lastName = childName.isLastName(expression);
			List<NodeAccess<T>> nextIteration = new NodeList();
			for (NodeAccess<T> current : currents) {
				NodeAccessList<T> list = current.findChildren(childName);
				if (logger.isLoggable(Level.FINE)) {
				    logger.fine(" -- [Element/select] from " + childName.childName() + " = " + list);
				}
				if (lastName && !list.isEmpty()) {
					if (logger.isLoggable(Level.FINE)) {
					    logger.fine(" [Element/select] end " + list);
					}
					return list;
				}
				nextIteration.add(list);
			}
			if (logger.isLoggable(Level.FINE)) {
			    logger.fine(" -- [Element/select] next = " + nextIteration);
			}
			currents = nextIteration;
		}
		return null;
	}

	private List<ObjectNode<T>> selectAll(List<NodeAccess<T>> start, String expression) {
		List<NodeAccess<T>> currents = start;
		for (ObjectName childName : new ObjectName(expression, delimiter)) {
			List<NodeAccess<T>> nextIteration = new ArrayList<NodeAccess<T>>();
			for (NodeAccess<T> current : currents) {
				nextIteration.addAll(current.findChildren(childName));
			}
			currents = nextIteration;
		}
		return currents.isEmpty() ? Collections.<ObjectNode<T>>emptyList() : new ArrayList<ObjectNode<T>>(currents);
	}

	public T value() {
		return evaluateNode(this);
	}

	public List<T> valueAll() {
		return evaluateNodeAll(this);
	}

	public T evaluate(String expression) {
		return evaluate(expression, null);
	}

	public T evaluate(String expression, T defaultValue) {
		ObjectNode<T> selected = select(expression);
		return selected == null || selected.value() == null ? defaultValue : selected.value();
	}

	public ObjectNode<T> child(int index) {
		Order order = order(index);
		return getElement(order.key, order.index);
	}

	@SuppressWarnings("unchecked")
	public List<ObjectNode<T>> children() {
		List<ObjectNode<T>> children = new ArrayList<ObjectNode<T>>();
		for (Order order : orderList()) {
			children.add(getElement(order.key, order.increment()));
		}
		return children;
	}

	public NodeAccessList<T> findChildren(ObjectName childName) {
		if (!childName.isConditional()) {
			if (childName.isAttribute()) {
				NodeAccess<T> attribute = getElement(childName.childName());
				return new NodeList(attribute == null ? Collections.<NodeAccess<T>>emptyList() : Arrays.asList(attribute), true);
			}
			return new NodeList(getList(childName.childName()), false);
		}
		if (childName.isIndexed()) {
			Order order = order(childName.childNameAsIndex());
			NodeAccess<T> element = getElement(order.key, order.index);
			return element == null ? new NodeList() : new NodeList(Arrays.asList(element), true);
		}
		return filter(Arrays.<NodeAccess<T>>asList(this), childName.childName());
	}

	@SuppressWarnings("unchecked")
	private T evaluateNode(Object value) {
		if (value instanceof Element) {
			return evaluateNode(((Element) value).getElement(""));
		} else if (Attribute.class.isInstance(value)) {
			return ((Attribute) value).value();
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private List<T> evaluateNodeAll(Object value) {
		List<T> valueList = new ArrayList<T>();
		if (value instanceof Element) {
			for (Object child : ((Element) value).getList("")) {
				valueList.add(evaluateNode(child));
			}
		} else {
			valueList.add(((Attribute) value).value());
		}
		return valueList;
	}

	// regard all as 'or' condition
	private NodeAccessList<T> filter(List<NodeAccess<T>> children, String condition) {
		Map<String, String> conditions = conditions(condition);
		NodeAccessList<T> filtered = new NodeList();
		for (NodeAccess<T> child : children) {
			for (Map.Entry<String, String> entry : conditions.entrySet()) {
				T value = child.evaluate(entry.getKey());
				if (value != null && value.equals(entry.getValue())) {
					filtered.add(child);
					break;
				}
			}
		}
		return filtered;
	}

	private class Attribute implements NodeAccess<T> {

		private String name;
		private T value;

		private Attribute(String name, T value) {
			this.name = name;
			this.value = value;
		}

		public String name() {
			return name;
		}

		public T value() {
			return value;
		}

		public List<T> valueAll() {
			return new ArrayList<T>(Arrays.asList(value));
		}

		public T evaluate(String expression) {
			throw new UnsupportedOperationException("evaluate '" + expression + "' for attribute");
		}

		public T evaluate(String expression, T defaultValue) {
			throw new UnsupportedOperationException("evaluate '" + expression + "' for attribute");
		}

		public ObjectNode<T> child(int index) {
			return null;
		}

		public List<ObjectNode<T>> children() {
			return Collections.emptyList();
		}

		public ObjectNode<T> select(String expression) {
			throw new UnsupportedOperationException("select for attribute");
		}

		public List<ObjectNode<T>> selectAll(String expression) {
			throw new UnsupportedOperationException("select for attribute");
		}

		public NodeAccessList<T> findChildren(ObjectName childName) {
			return new NodeList();
		}

		public String toString() {
			return String.valueOf(value);
		}
	}

	private class Order {
		private String key;
		private int index;
		private Order(String key) { this.key = key; }
		private int increment() { return index++; }
		@SuppressWarnings("unchecked")
		public boolean equals(Object object) { return key.equals(((Order) object).key); }
	}

	private Map<String, String> conditions(String condition) {
		Map<String, String> property = new HashMap<String, String>();
		for (String prop : condition.split(",")) {
			int index = prop.indexOf('=');
			if (index > 0 && index < prop.length()) {
				String value = prop.substring(index + 1).trim();
				property.put(prop.substring(0, index).trim(), value.substring(1, value.length() - 1));
			}
		}
		return property;
	}

	@Override
	public Iterator<Entry<String, NodeAccess<T>>> iterator() {
        return iterator("");
	}

    public Iterator<Entry<String, NodeAccess<T>>> iterator(final String parentName) {
		final Iterator<Entry<String, NodeAccess<T>>> iterator = super.iterator();
		return new Iterator<Entry<String, NodeAccess<T>>>() {

			private Iterator<Entry<String, NodeAccess<T>>> current;

			public boolean hasNext() {
				return current != null && current.hasNext() || iterator.hasNext();
			}

			@SuppressWarnings("unchecked")
			public Entry<String, NodeAccess<T>> next() {
				if (current != null && current.hasNext()) {
					return returnEntry(current.next());
				}
				Entry<String, NodeAccess<T>> next = iterator.next();
				if (next.getValue() instanceof Element) {
					Element<T> element = (Element<T>) next.getValue();
                    current = element.iterator(name(next.getKey()));
				}
				return returnEntry(next);
			}

			private Entry<String, NodeAccess<T>> returnEntry(Entry<String, NodeAccess<T>> next) {
                if (!(next instanceof SimpleEntry)) {
                    return new SimpleEntry<String, NodeAccess<T>>(name(next.getKey()), next.getValue());
				}
				return next;
			}

			private String name(String name) {
                return parentName.length() == 0 ? name : parentName + delimiter + name;
			}

			public void remove() {
				throw new UnsupportedOperationException("remove");
			}
		};
	}

	private class NodeList extends ArrayList<NodeAccess<T>> implements NodeAccessList<T> {

		private boolean singlet;

		private NodeList() { }
		private NodeList(List<NodeAccess<T>> back, boolean singlet) { super(back); this.singlet = singlet; }

		public NodeAccessList<T> findChildren(ObjectName childName) {
			if (!childName.isConditional()) {
				NodeAccessList<T> result = new NodeList();
				for (NodeAccess<T> access : this) {
					result.addAll(access.findChildren(childName));
				}
				return result;
			}
			if (childName.isIndexed()) {
				int index = childName.childNameAsIndex();
				if (index < size()) {
					return new NodeList(Arrays.asList(get(index)), true);
				}
				return new NodeList();
			}
			return filter(this, childName.childName());
		}

		public String name() {
			return get(0).name();		// all has same name
		}

		public T value() {
			return get(0).value();		// value of first element
		}

		public List<T> valueAll() {
			List<T> values = new ArrayList<T>();
			for (NodeAccess<T> element : this) {
				values.addAll(element.valueAll());
			}
			return values;
		}

		public ObjectNode<T> child(int index) {
			return singlet ? get(0).child(index) : get(index);
		}

		public List<ObjectNode<T>> children() {
			List<ObjectNode<T>> values = new ArrayList<ObjectNode<T>>();
			for (NodeAccess<T> element : this) {
				values.addAll(element.children());
			}
			return values;
		}

		public ObjectNode<T> select(String expression) {
			ObjectName[] objectNames = new ObjectName(expression, delimiter).split(1);
			NodeAccessList<T> selected = findChildren(objectNames[0]);
			return selected.isEmpty() ? null : Element.this.select(selected, objectNames[1].name());
		}

		public List<ObjectNode<T>> selectAll(String expression) {
			ObjectName[] objectNames = new ObjectName(expression, delimiter).split(1);
			NodeAccessList<T> selected = findChildren(objectNames[0]);
			return Element.this.selectAll(selected, objectNames[1].name());
		}

		public T evaluate(String expression) {
			return evaluate(expression, null);
		}

		public T evaluate(String expression, T defaultValue) {
			ObjectNode<T> selected = select(expression);
			return selected != null ? selected.value() : null;
		}

		public String toString() {
			return getClass().getSimpleName() + (singlet ? "" : "(" + size() + ")") + super.toString();
		}
	}
}
